﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using Pfz.Extensions;
using Pfz.Threading;

namespace Pfz.Remoting.Udp
{
	/// <summary>
	/// A listener of TcpWithUdpConnections.
	/// </summary>
	public sealed class GuaranteedUdpListener:
		ThreadSafeExceptionAwareDisposable,
		IUdpConnectionListener<GuaranteedUdpConnection>
	{
		internal Dictionary<IPEndPoint, GuaranteedUdpConnection> _connections = new Dictionary<IPEndPoint, GuaranteedUdpConnection>();
		internal UdpClient _udp;
		internal readonly int _pendingGuaranteedDeliveryCount;
		internal readonly int _confirmImmediatelyCount;

		/// <summary>
		/// Creates a new instance of this class, setting the port to listen to and the number of Udp packets that must be sent before
		/// waiting for a confirmation.
		/// </summary>
		public GuaranteedUdpListener(int port, int pendingGuaranteedDeliveryCount=57)
		{
			if (pendingGuaranteedDeliveryCount < 2)
				throw new ArgumentException("pendingGuaranteedDeliveryCount must be at least 2.", "pendingGuaranteedDeliveryCount");

			_pendingGuaranteedDeliveryCount = pendingGuaranteedDeliveryCount;
			_confirmImmediatelyCount = pendingGuaranteedDeliveryCount/2;

			_udp = new UdpClient(port);
			UnlimitedThreadPool.Run(_KeepReceiving);
		}

		/// <summary>
		/// Releases the listener.
		/// </summary>
		protected override void Dispose(bool disposing)
		{
			if (disposing)
			{
				if (_newConnectionsEvent != null)
					_newConnectionsEvent.Dispose();

				Disposer.Dispose(ref _udp);

				var connections = _connections;
				if (connections != null)
				{
					_connections = null;
					foreach(var connection in connections.Values)
						connection.Dispose();
				}
			}

			base.Dispose(disposing);
		}

		private void _KeepReceiving()
		{
			try
			{
				while(true)
				{
					if (WasDisposed)
						return;

					IPEndPoint endpoint = null;
					byte[] received;
					try
					{
						received = _udp.Receive(ref endpoint);
					}
					catch(SocketException)
					{
						continue;
					}

					if (received == null)
						return;

					GuaranteedUdpConnection connection;
					lock(DisposeLock)
					{
						if (WasDisposed)
							return;

						connection = _connections.GetValueOrDefault(endpoint);
						if (connection == null)
						{
							_newConnectionsQueue.Enqueue(endpoint);
							_newConnectionsEvent.Set();
							continue;
						}
					}

					connection._StoreReceived(received);
				}
			}
			catch(Exception exception)
			{
				Dispose(exception);
			}
			finally
			{
				Dispose();
			}
		}

		private readonly ManagedAutoResetEvent _newConnectionsEvent = new ManagedAutoResetEvent();
		private Queue<IPEndPoint> _newConnectionsQueue = new Queue<IPEndPoint>();

		/// <summary>
		/// Tries to accept a new connection. May return null if the listener is disposed.
		/// </summary>
		public GuaranteedUdpConnection TryAccept()
		{
			while(true)
			{
				lock(DisposeLock)
				{
					if (WasDisposed)
						return null;

					if (_newConnectionsQueue.Count > 0)
					{
						var endpoint = _newConnectionsQueue.Dequeue();

						if (_connections.ContainsKey(endpoint))
							continue;

						var result = new GuaranteedUdpConnection(this, endpoint);
						try
						{
							_connections.Add(endpoint, result);
						}
						catch
						{
							result.Dispose();
							throw;
						}
						return result;
					}
				}

				_newConnectionsEvent.WaitOne();
			}
		}

		/// <summary>
		/// Gets a copy of all active connections at this moment.
		/// </summary>
		public GuaranteedUdpConnection[] GetConnections()
		{
			lock(DisposeLock)
			{
				if (WasDisposed)
					return null;

				return _connections.Values.ToArray();
			}
		}
	}
}
